// app/pdftron-viewer/page.tsx "use client" import * as React from "react" import { useSearchParams } from "next/navigation" import { Suspense } from "react" import { Button } from "@/components/ui/button" import { ArrowLeft, MessageSquare, Download, Upload } from "lucide-react" import { Badge } from "@/components/ui/badge" import { useSession } from "next-auth/react" import { useToast } from "@/hooks/use-toast" import type { WebViewerInstance } from "@pdftron/webviewer" // PDFTron 코멘트 타입 정의 interface PDFTronComment { id: number documentReviewId: number pdftronDocumentId: string xfdfString: string annotationData: any commentSummary?: { total: number open: number resolved: number rejected: number deferred: number byCategory: Record bySeverity: Record byAuthor: Record } createdBy: number createdByName?: string createdByType: "buyer" | "vendor" createdAt: Date updatedAt: Date } // PDFTronViewer 컴포넌트 (내부에서 useSearchParams 사용) function PDFTronViewer() { const { data: session, status } = useSession() const searchParams = useSearchParams() const viewerRef = React.useRef(null) const [instance, setInstance] = React.useState(null) const [isLoading, setIsLoading] = React.useState(true) const [lastSavedTime, setLastSavedTime] = React.useState(null) const [isSaving, setIsSaving] = React.useState(false) const [annotationCount, setAnnotationCount] = React.useState(0) const { toast } = useToast() const initialized = React.useRef(false) const isCancelled = React.useRef(false) const autoSaveTimerRef = React.useRef(null) const xfdfLoadedRef = React.useRef(false) // XFDF 로딩 완료 여부 추적 // URL 파라미터에서 정보 가져오기 const filePath = searchParams.get('filePath') const documentId = searchParams.get('documentId') const documentReviewId = searchParams.get('documentReviewId') const sessionId = searchParams.get('sessionId') const documentName = searchParams.get('documentName') // PDFTron WebViewer 초기화 - session과 XFDF 모두 준비된 후 실행 React.useEffect(() => { if (!initialized.current && viewerRef.current && filePath && session && documentReviewId) { initialized.current = true isCancelled.current = false // XFDF 먼저 로드한 후 WebViewer 초기화 loadAndInitializeViewer() } return () => { if (instance) { try { instance.UI.dispose() } catch (error) { console.warn("Error disposing viewer:", error) } } isCancelled.current = true // 타이머 정리 if (autoSaveTimerRef.current) { clearTimeout(autoSaveTimerRef.current) } } }, [filePath, session, documentReviewId, sessionId]) const loadAndInitializeViewer = async () => { try { // 1. 먼저 기존 XFDF 로드 let existingXFDF = "" try { const response = await fetch(`/api/pdftron-comments/xfdf?documentReviewId=${documentReviewId}`) if (response.ok) { const data = await response.json() if (data.xfdfString) { existingXFDF = data.xfdfString console.log("Loaded existing XFDF successfully") } } } catch (error) { console.error("Failed to load XFDF:", error) } // 2. WebViewer 초기화 await initializeWebViewer(existingXFDF) } catch (error) { console.error("Failed to initialize viewer:", error) setIsLoading(false) toast({ title: "Error", description: "Failed to initialize document viewer", variant: "destructive" }) } } const initializeWebViewer = async (existingXFDF: string) => { try { console.log("Starting WebViewer initialization...") console.log("File path:", filePath) console.log("Current session:", session) console.log("Has existing XFDF:", !!existingXFDF) // 동적 import 사용 const { default: WebViewer } = await import("@pdftron/webviewer") if (isCancelled.current || !viewerRef.current) { console.log("WebViewer initialization cancelled") return } // WebViewer 인스턴스 생성 const webviewerInstance = await WebViewer( { path: "/pdftronWeb", licenseKey: process.env.NEXT_PUBLIC_PDFTRON_LICENSE_KEY || process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY, initialDoc: filePath!, }, viewerRef.current ) if (isCancelled.current) { console.log("WebViewer initialization cancelled after creation") return } setInstance(webviewerInstance) if (!webviewerInstance.Core) { console.error("WebViewer Core is not available") setIsLoading(false) return } const { documentViewer, annotationManager, Annotations } = webviewerInstance.Core // 현재 사용자 설정 const currentUser = session?.user?.email || session?.user?.name || 'Anonymous' console.log("Setting current user:", currentUser) annotationManager.setCurrentUser(currentUser) // 권한 설정 - 자기 annotation만 수정/삭제 가능 annotationManager.setPermissionCheckCallback((author: string, annotation: any) => { // 자기가 만든 annotation만 수정 가능 return author === currentUser }) // 문서 로드 완료 시 documentViewer.addEventListener('documentLoaded', async () => { console.log("Document loaded successfully") setIsLoading(false) console.log(existingXFDF) // 기존 XFDF 적용 if (existingXFDF && !xfdfLoadedRef.current) { console.log(existingXFDF, "existingXFDF") try { await annotationManager.importAnnotations(existingXFDF) xfdfLoadedRef.current = true console.log("Imported existing annotations from XFDF") // 초기 annotation 수 설정 const annotations = annotationManager.getAnnotationsList() setAnnotationCount(annotations.length) // 마지막 저장 시간 설정 setLastSavedTime(new Date()) } catch (error) { console.error("Failed to import XFDF:", error) toast({ title: "Warning", description: "Failed to load existing annotations", variant: "destructive" }) } } // UI 설정 (1초 지연) setTimeout(() => { setupUI() }, 1000) }) // UI 설정 함수 const setupUI = async () => { try { console.log("Setting up UI features...") // Review 모드 annotation 도구 활성화 try { // 주석 도구 활성화 webviewerInstance.UI.enableElements(['highlightToolButton']) webviewerInstance.UI.enableElements(['stickyToolButton']) webviewerInstance.UI.enableElements(['freeTextToolButton']) webviewerInstance.UI.enableElements(['underlineToolButton']) webviewerInstance.UI.enableElements(['strikeoutToolButton']) webviewerInstance.UI.enableElements(['squigglyToolButton']) // 노트 패널 열기 webviewerInstance.UI.openElements(['notesPanel']) } catch (e) { console.log("Could not enable annotation tools:", e) } // 커스텀 이벤트 리스너 설정 setupAnnotationListeners() } catch (error) { console.error("Error setting up UI:", error) } } // Annotation 이벤트 리스너 설정 const setupAnnotationListeners = () => { // 자동 저장 함수 const handleAutoSave = async () => { if (!documentReviewId) { console.log("No documentReviewId, skipping auto-save") return } // 이미 저장 중이면 스킵 if (isSaving) { console.log("Already saving, skipping...") return } setIsSaving(true) try { const xfdfString = await annotationManager.exportAnnotations() // Annotation 요약 정보 생성 const annotations = annotationManager.getAnnotationsList() const summary = { total: annotations.length, open: annotations.filter((a: any) => a.getCustomData('status') !== 'resolved').length, resolved: annotations.filter((a: any) => a.getCustomData('status') === 'resolved').length, rejected: annotations.filter((a: any) => a.getCustomData('status') === 'rejected').length, deferred: annotations.filter((a: any) => a.getCustomData('status') === 'deferred').length, byCategory: {} as Record, bySeverity: {} as Record, byAuthor: {} as Record } annotations.forEach((annotation: any) => { const category = annotation.getCustomData('category') || 'general' const severity = annotation.getCustomData('severity') || 'minor' const author = annotation.Author || 'Anonymous' summary.byCategory[category] = (summary.byCategory[category] || 0) + 1 summary.bySeverity[severity] = (summary.bySeverity[severity] || 0) + 1 summary.byAuthor[author] = (summary.byAuthor[author] || 0) + 1 }) // 서버에 저장 const response = await fetch('/api/pdftron-comments/xfdf', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ documentReviewId: parseInt(documentReviewId), sessionId: sessionId ? parseInt(sessionId) :0, pdftronDocumentId: documentId, xfdfString: xfdfString, commentSummary: summary, createdByType: 'buyer' }) }) if (response.ok) { setLastSavedTime(new Date()) setAnnotationCount(annotations.length) console.log("Auto-save successful") } else { console.error("Auto-save failed") toast({ title: "Error", description: "Failed to save annotations", variant: "destructive" }) } } catch (error) { console.error("Auto-save error:", error) toast({ title: "Error", description: "Failed to save annotations", variant: "destructive" }) } finally { setIsSaving(false) } } // Annotation 변경 감지 annotationManager.addEventListener('annotationChanged', (annotations: any[], action: string) => { if (action === 'add' || action === 'modify' || action === 'delete') { // 새 annotation에 기본 메타데이터 추가 if (action === 'add') { annotations.forEach(annotation => { if (!annotation.getCustomData('category')) { annotation.setCustomData('category', 'general') annotation.setCustomData('severity', 'minor') annotation.setCustomData('status', 'open') annotation.setCustomData('createdBy', session?.user?.id || '') annotation.setCustomData('createdByType', 'buyer') annotation.setCustomData('createdAt', new Date().toISOString()) // 기본 색상 설정 (minor = yellow) try { if (Annotations) { annotation.Color = new Annotations.Color(250, 204, 21) } } catch (e) { console.log("Could not set annotation color") } } }) } // 자동 저장 - 2초 디바운싱 if (autoSaveTimerRef.current) { clearTimeout(autoSaveTimerRef.current) } autoSaveTimerRef.current = setTimeout(() => { console.log("Auto-saving annotations...") handleAutoSave() }, 2000) } }) // 코멘트 변경 감지 annotationManager.addEventListener('annotationCommentsChanged', () => { // 자동 저장 - 1.5초 디바운싱 if (autoSaveTimerRef.current) { clearTimeout(autoSaveTimerRef.current) } autoSaveTimerRef.current = setTimeout(() => { console.log("Auto-saving comments...") handleAutoSave() }, 1500) }) // Annotation 선택 시 기본값 설정 annotationManager.addEventListener('annotationSelected', (annotations: any, action: string) => { if (annotations && annotations.length > 0) { const annotation = annotations[0] // 기본 커스텀 데이터 설정 if (!annotation.getCustomData('category')) { annotation.setCustomData('category', 'general') annotation.setCustomData('severity', 'minor') annotation.setCustomData('status', 'open') annotation.setCustomData('createdBy', session?.user?.id || '') annotation.setCustomData('createdByType', 'buyer') annotation.setCustomData('createdAt', new Date().toISOString()) } } }) } } catch (error) { console.error("WebViewer initialization failed:", error) setIsLoading(false) toast({ title: "Error", description: "Failed to initialize document viewer", variant: "destructive" }) } } // 통계 정보 가져오기 const getAnnotationStats = () => { if (!instance) return null const { annotationManager } = instance.Core const annotations = annotationManager.getAnnotationsList() return { total: annotations.length, open: annotations.filter((a: any) => a.getCustomData('status') !== 'resolved').length, resolved: annotations.filter((a: any) => a.getCustomData('status') === 'resolved').length } } // 시간 포맷팅 const formatLastSaved = () => { if (!lastSavedTime) return null const now = new Date() const diff = Math.floor((now.getTime() - lastSavedTime.getTime()) / 1000) if (diff < 60) return "Just saved" if (diff < 3600) return `Saved ${Math.floor(diff / 60)} min ago` if (diff < 86400) return `Saved ${Math.floor(diff / 3600)} hours ago` return `Saved ${Math.floor(diff / 86400)} days ago` } const stats = getAnnotationStats() const lastSavedText = formatLastSaved() return (
{/* Header */}

{documentName || 'Document Viewer'}

Review Mode User: {session?.user?.email || session?.user?.name || 'Loading...'} {stats && stats.total > 0 && ( <> {stats.open} open / {stats.total} total )} {isSaving && ( <>
Auto-saving...
)} {!isSaving && lastSavedText && ( <> ✓ {lastSavedText} )}
{/* PDFTron Viewer */}
{(isLoading || status === "loading") && (

{status === "loading" ? "Loading session..." : "Loading document..."}

Initializing PDFTron viewer...

)}
) } // 메인 페이지 컴포넌트 (Suspense로 PDFTronViewer 감싸기) export default function PDFTronViewerPage() { return (

Document Viewer

Loading...

Loading PDF viewer...

}> ) }